package org.tulkas.http;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.StringReader;
import java.util.ArrayList;
import java.util.Date;
import java.util.Iterator;
import java.util.List;
import java.util.TreeSet;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
import java.util.zip.ZipOutputStream;

import org.apache.http.Header;
import org.apache.http.annotation.GuardedBy;
import org.apache.http.client.CookieStore;
import org.apache.http.cookie.Cookie;
import org.apache.http.cookie.CookieIdentityComparator;
import org.apache.http.cookie.CookieOrigin;
import org.apache.http.cookie.CookieSpec;
import org.apache.http.cookie.MalformedCookieException;
import org.apache.http.impl.cookie.BestMatchSpec;
import org.apache.http.message.BasicLineParser;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.tulkas.flusher.IFlusher;
import org.tulkas.util.FileUtil;

/**
 * @author <a href="mailto:tulkas.web@gmail.com">Tulkas</a>
 *
 */
public class CookieStorage implements CookieStore, IFlusher {
	public static final String COOKIE_FILE = "cookieStore.cookie";

	private static final long serialVersionUID = 1002271641810886905L;

	private static Logger logger = LoggerFactory.getLogger(CookieStorage.class);
	private static final CookieSpec DEFAULT_PARSER_SPEC = new BestMatchSpec();

	@GuardedBy("this")
	private final TreeSet<Cookie> cookies = new TreeSet<Cookie>(
			new CookieIdentityComparator());

	private String clientName;
	private boolean persistent;
	private boolean modified;

	CookieStorage(String clientName) {
		this.clientName = clientName;
		load();
	}

	public static Cookie parseCookie(String cookieString)
			throws MalformedCookieException {
		Cookie[] ret = parseCookies("Set-Cookie: " + cookieString);
		if (ret.length == 1)
			return ret[0];
		else
			throw new MalformedCookieException(
					"Can not parse any cookies or more than one cookie in ["
							+ cookieString + "]");

	}

	/* (non-Javadoc)
	 * @see org.tulkas.robot.CookieStore#parseCookies(java.lang.String)
	 */
	public static Cookie[] parseCookies(String setCookiesStrings)
			throws MalformedCookieException {
		BufferedReader reader = new BufferedReader(new StringReader(
				setCookiesStrings));
		String line = null;
		List<Cookie> list = new ArrayList<Cookie>();
		try {
			while ((line = reader.readLine()) != null) {
				Header header = BasicLineParser.parseHeader(line, null);
				CookieOrigin co = new CookieOrigin("unknownHost", 80, "/",
						false);
				List<Cookie> cookies = DEFAULT_PARSER_SPEC.parse(header, co);
				if (cookies.size() >= 1)
					list.add(cookies.get(0));
			}
		} catch (IOException e) {
			throw new MalformedCookieException(
					"Can not parse set-cookies strings: " + setCookiesStrings);
		} finally {
			try {
				reader.close();
			} catch (IOException e) {
			}
		}

		Cookie[] ret = new Cookie[list.size()];
		list.toArray(ret);

		return ret;
	}

	public synchronized boolean isPersistent() {
		return persistent;
	}

	public synchronized void setPersistent(boolean persistent) {
		this.persistent = persistent;
	}

	/* (non-Javadoc)
	 * @see org.tulkas.robot.CookieStore#addCookies(javax.servlet.http.Cookie[])
	 */
	public synchronized void addCookies(Cookie[] cookieString) {
		if (cookieString != null) {
			for (Cookie cooky : cookieString) {
				this.addCookie(cooky);
			}
		}
		modified = true;
	}

	/* (non-Javadoc)
	 * @see org.tulkas.robot.CookieStore#clearInDomain(java.lang.String)
	 */
	public synchronized boolean clearInDomain(String domain) {
		if (domain == null) {
			return false;
		}
		boolean removed = false;
		for (Iterator<Cookie> it = cookies.iterator(); it.hasNext();) {
			if (domain.equals(it.next().getDomain())) {
				it.remove();
				removed = true;
			}
		}
		modified = true;
		return removed;
	}

	/* (non-Javadoc)
	 * @see org.tulkas.robot.CookieStore#getCookiesInDomain(java.lang.String)
	 */
	public synchronized List<Cookie> getCookiesInDomain(String domain) {
		ArrayList<Cookie> ret = new ArrayList<Cookie>();
		for (Iterator<Cookie> it = cookies.iterator(); it.hasNext();) {
			Cookie cooky = it.next();
			if (domain.equals(cooky.getDomain()))
				ret.add(cooky);
		}
		return ret;
	}

	/* (non-Javadoc)
	 * @see org.apache.http.client.CookieStore#addCookie(org.apache.http.cookie.Cookie)
	 */
	public synchronized void addCookie(Cookie cookie) {
		if (cookie != null) {
			// first remove any old cookie that is equivalent
			cookies.remove(cookie);
			if (!cookie.isExpired(new Date())) {
				cookies.add(cookie);
			}
		}
		modified = true;
	}

	/* (non-Javadoc)
	 * @see org.apache.http.client.CookieStore#clear()
	 */
	public synchronized void clear() {
		cookies.clear();
		modified = true;
	}

	/* (non-Javadoc)
	 * @see org.apache.http.client.CookieStore#clearExpired(java.util.Date)
	 */
	public synchronized boolean clearExpired(Date date) {
		if (date == null) {
			return false;
		}
		boolean removed = false;
		for (Iterator<Cookie> it = cookies.iterator(); it.hasNext();) {
			if (it.next().isExpired(date)) {
				it.remove();
				removed = true;
			}
		}
		modified = true;
		return removed;
	}

	/* (non-Javadoc)
	 * @see org.apache.http.client.CookieStore#getCookies()
	 */
	public synchronized List<Cookie> getCookies() {
		return new ArrayList<Cookie>(cookies);
	}

	private synchronized List<Cookie> getPersistentCookies() {
		List<Cookie> ret = new ArrayList<Cookie>();
		for (Cookie cookie : cookies) {
			if (cookie.isPersistent())
				ret.add(cookie);
		}
		return ret;
	}

	public synchronized void save() {
		String persistFile = getFilePath();
		try {
			new File(FileUtil.getFileDir(persistFile)).mkdirs();
			ZipOutputStream zos = new ZipOutputStream(new FileOutputStream(
					new File(persistFile)));
			ZipEntry entry = new ZipEntry(this.getClass().getName());
			zos.putNextEntry(entry);
			ObjectOutputStream oos = new ObjectOutputStream(zos);

			List<Cookie> cookies = getPersistentCookies();
			oos.writeInt(cookies.size());
			for (int i = 0; i < cookies.size(); i++) {
				oos.writeObject(cookies.get(i));
			}
			zos.closeEntry();
			oos.close();
		} catch (IOException e) {
			logger.error("Error saving cookie file [" + persistFile + "]!", e);
		}
	}

	private String getFilePath() {
		return ClientManager.getInstance().getClientPath(clientName) + "/"
				+ COOKIE_FILE;
	}

	public synchronized void load() {
		File file = new File(getFilePath());
		try {
			if (file.exists()) {
				ZipInputStream zis = new ZipInputStream(new FileInputStream(
						file));
				if (zis.getNextEntry() != null) {
					ObjectInputStream ois = new ObjectInputStream(zis);
					int count = ois.readInt();
					for (int i = 0; i < count; i++) {
						Cookie cookie = (Cookie) ois.readObject();
						addCookie(cookie);
					}
				}
				zis.closeEntry();
				zis.close();
			}
		} catch (IOException e) {
			logger.error(
					"Error loading cookie file [" + file.toString() + "]!", e);
		} catch (ClassNotFoundException e) {
			logger.error(
					"Error loading cookie file [" + file.toString() + "]!", e);
		}
	}

	public synchronized void flush() {
		save();
		modified = false;
	}

	public synchronized boolean isAvaliable() {
		return true;
	}

	public synchronized boolean isModified() {
		return modified;
	}
}
